// Copyright 2019 The Go Authors. All rights reserved.
// Use of this session code is governed by a BSD-style
// license that can be found in the LICENSE file.

package session

import (
	"context"
	"crypto/sha256"
	"fmt"
	"io/ioutil"
	"mi/compiler/ast"
	"mi/internal/xpath"
	"os"
	"reflect"
	"strconv"
	"sync"
	"sync/atomic"
	"time"

	"mi/internal/event"
	"mi/internal/lsp/debug/tag"
	"mi/internal/memoize"
)

func New(ctx context.Context, options func(*Options)) *Cache {
	index := atomic.AddInt64(&cacheIndex, 1)
	c := &Cache{
		id:          strconv.FormatInt(index, 10),
		options:     options,
		fileContent: map[xpath.URI]*fileHandle{},
	}
	return c
}

type Cache struct {
	id      string
	options func(*Options)

	store memoize.Store

	fileMu      sync.Mutex
	fileContent map[xpath.URI]*fileHandle
}

func (c *Cache) GetFile(ctx context.Context, uri xpath.URI) (FileHandle, error) {
	return c.getFile(ctx, uri)
}

func (c *Cache) getFile(ctx context.Context, uri xpath.URI) (*fileHandle, error) {
	fi, statErr := os.Stat(uri.Filename())
	if statErr != nil {
		return &fileHandle{
			err: statErr,
			uri: uri,
		}, nil
	}

	c.fileMu.Lock()
	fh, ok := c.fileContent[uri]
	c.fileMu.Unlock()
	if ok && fh.modTime.Equal(fi.ModTime()) {
		return fh, nil
	}

	fh, err := readFile(ctx, uri, fi.ModTime())
	if err != nil {
		return nil, err
	}
	c.fileMu.Lock()
	c.fileContent[uri] = fh
	c.fileMu.Unlock()
	return fh, nil
}

// ioLimit limits the number of parallel file reads per process.
var ioLimit = make(chan struct{}, 128)

func readFile(ctx context.Context, uri xpath.URI, modTime time.Time) (*fileHandle, error) {
	select {
	case ioLimit <- struct{}{}:
	case <-ctx.Done():
		return nil, ctx.Err()
	}
	defer func() { <-ioLimit }()

	ctx, done := event.Start(ctx, "cache.readFile", tag.File.Of(uri.Filename()))
	_ = ctx
	defer done()

	data, err := ioutil.ReadFile(uri.Filename())
	if err != nil {
		return &fileHandle{
			modTime: modTime,
			err:     err,
		}, nil
	}
	return &fileHandle{
		modTime: modTime,
		uri:     uri,
		bytes:   data,
		hash:    hashContents(data),
	}, nil
}

func (c *Cache) NewSession(ctx context.Context) Session {
	index := atomic.AddInt64(&sessionIndex, 1)
	// modify it!!!
	options := &Options{} //session.DefaultOptions().Clone()
	if c.options != nil {
		c.options(options)
	}
	s := &session{
		cache:             c,
		id:                strconv.FormatInt(index, 10),
		options:           options,
		overlays:          make(map[xpath.URI]*overlay),
		cacheSourceFiles:  make(map[string]*ast.SourceFile),
		cachePackages:     make(map[string]*ast.Package),
		cacheProjectFiles: make(map[string]*ast.ProjectFile),
	}
	event.Log(ctx, "New session", KeyCreateSession.Of(s))
	return s
}

func hashContents(contents []byte) string {
	return fmt.Sprintf("%x", sha256.Sum256(contents))
}

var cacheIndex, sessionIndex, viewIndex int64

func (c *Cache) ID() string                     { return c.id }
func (c *Cache) MemStats() map[reflect.Type]int { return c.store.Stats() }

type fileHandle struct {
	modTime time.Time
	uri     xpath.URI
	bytes   []byte
	hash    string
	err     error
}

func (h *fileHandle) Saved() bool {
	return true
}

func (h *fileHandle) URI() xpath.URI {
	return h.uri
}

func (h *fileHandle) Kind() FileKind {
	return DetectLanguage("", h.uri.Filename())
}

func (h *fileHandle) Hash() string {
	return h.hash
}

func (h *fileHandle) FileIdentity() FileIdentity {
	return FileIdentity{
		URI:  h.uri,
		Hash: h.hash,
		Kind: h.Kind(),
	}
}

func (h *fileHandle) Read() ([]byte, error) {
	return h.bytes, h.err
}

